/*
 * @(#)Cookie2.java   0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal�r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/
 *
 */

package org.everrest.http.client;

import org.everrest.core.util.Logger;

import java.io.UnsupportedEncodingException;
import java.net.ProtocolException;
import java.util.Date;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * This class represents an http cookie as specified in the <A
 * HREF="http://www.ietf.org/rfc/rfc2965.txt">HTTP State Management Mechanism
 * spec</A> (also known as a version 1 cookie).
 *
 * @version 0.3-3 06/05/2001
 * @author Ronald Tschal�r
 * @since V0.3
 */
public class Cookie2 extends Cookie
{
   /** Make this compatible with V0.3-2 */
   private static final long serialVersionUID = 2208203902820875917L;

   private static final Logger log = Logger.getLogger(Cookie2.class);

   protected int version;

   protected boolean discard;

   protected String comment;

   protected URI comment_url;

   protected int[] port_list;

   protected String port_list_str;

   protected boolean path_set;

   protected boolean port_set;

   protected boolean domain_set;

   /**
    * Create a cookie.
    *
    * @param name the cookie name
    * @param value the cookie value
    * @param domain the host this cookie will be sent to
    * @param port_list an array of allowed server ports for this cookie, or null
    *        if the the cookie may be sent to any port
    * @param path the path prefix for which this cookie will be sent
    * @param epxires the Date this cookie expires, or null if never
    * @param discard if true then the cookie will be discarded at the end of the
    *        session regardless of expiry
    * @param secure if true this cookie will only be over secure connections
    * @param comment the comment associated with this cookie, or null if none
    * @param comment_url the comment URL associated with this cookie, or null if
    *        none
    * @exception NullPointerException if <var>name</var>, <var>value</var>,
    *            <var>domain</var>, or <var>path</var> is null
    */
   public Cookie2(String name, String value, String domain, int[] port_list, String path, Date expires,
      boolean discard, boolean secure, String comment, URI comment_url)
   {
      super(name, value, domain, path, expires, secure);

      this.discard = discard;
      this.port_list = port_list;
      this.comment = comment;
      this.comment_url = comment_url;

      path_set = true;
      domain_set = true;

      if (port_list != null && port_list.length > 0)
      {
         StringBuffer tmp = new StringBuffer();
         tmp.append(port_list[0]);
         for (int idx = 1; idx < port_list.length; idx++)
         {
            tmp.append(',');
            tmp.append(port_list[idx]);
         }

         port_list_str = tmp.toString();
         port_set = true;
      }

      version = 1;
   }

   /**
    * Use <code>parse()</code> to create cookies.
    *
    * @see #parse(java.lang.String, HTTPClient.RoRequest)
    */
   protected Cookie2(RoRequest req)
   {
      super(req);

      path = Util.getPath(req.getRequestURI());
      int slash = path.lastIndexOf('/');
      if (slash != -1)
         path = path.substring(0, slash + 1);
      if (domain.indexOf('.') == -1)
         domain += ".local";

      version = -1;
      discard = false;
      comment = null;
      comment_url = null;
      port_list = null;
      port_list_str = null;

      path_set = false;
      port_set = false;
      domain_set = false;
   }

   /**
    * Parses the Set-Cookie2 header into an array of Cookies.
    *
    * @param set_cookie the Set-Cookie2 header received from the server
    * @param req the request used
    * @return an array of Cookies as parsed from the Set-Cookie2 header
    * @exception ProtocolException if an error occurs during parsing
    */
   protected static Cookie[] parse(String set_cookie, RoRequest req) throws ProtocolException
   {
      Vector cookies;
      try
      {
         cookies = Util.parseHeader(set_cookie);
      }
      catch (ParseException pe)
      {
         throw new ProtocolException(pe.getMessage());
      }

      Cookie cookie_arr[] = new Cookie[cookies.size()];
      int cidx = 0;
      for (int idx = 0; idx < cookie_arr.length; idx++)
      {
         HttpHeaderElement c_elem = (HttpHeaderElement)cookies.elementAt(idx);

         // set NAME and VALUE

         if (c_elem.getValue() == null)
            throw new ProtocolException("Bad Set-Cookie2 header: " + set_cookie + "\nMissing value " + "for cookie '"
               + c_elem.getName() + "'");
         Cookie2 curr = new Cookie2(req);
         curr.name = c_elem.getName();
         curr.value = c_elem.getValue();

         // set all params

         NVPair[] params = c_elem.getParams();
         boolean discard_set = false, secure_set = false;
         for (int idx2 = 0; idx2 < params.length; idx2++)
         {
            String name = params[idx2].getName().toLowerCase();

            // check for required value parts
            if ((name.equals("version") || name.equals("max-age") || name.equals("domain") || name.equals("path")
               || name.equals("comment") || name.equals("commenturl"))
               && params[idx2].getValue() == null)
            {
               throw new ProtocolException("Bad Set-Cookie2 header: " + set_cookie + "\nMissing value " + "for "
                  + params[idx2].getName() + " attribute in cookie '" + c_elem.getName() + "'");
            }

            if (name.equals("version")) // Version
            {
               if (curr.version != -1)
                  continue;
               try
               {
                  curr.version = Integer.parseInt(params[idx2].getValue());
               }
               catch (NumberFormatException nfe)
               {
                  throw new ProtocolException("Bad Set-Cookie2 header: " + set_cookie + "\nVersion '"
                     + params[idx2].getValue() + "' not a number");
               }
            }
            else if (name.equals("path")) // Path
            {
               if (curr.path_set)
                  continue;
               curr.path = params[idx2].getValue();
               curr.path_set = true;
            }
            else if (name.equals("domain")) // Domain
            {
               if (curr.domain_set)
                  continue;
               String d = params[idx2].getValue().toLowerCase();

               // add leading dot if not present and if domain is
               // not the full host name
               if (d.charAt(0) != '.' && !d.equals(curr.domain))
                  curr.domain = "." + d;
               else
                  curr.domain = d;
               curr.domain_set = true;
            }
            else if (name.equals("max-age")) // Max-Age
            {
               if (curr.expires != null)
                  continue;
               int age;
               try
               {
                  age = Integer.parseInt(params[idx2].getValue());
               }
               catch (NumberFormatException nfe)
               {
                  throw new ProtocolException("Bad Set-Cookie2 header: " + set_cookie + "\nMax-Age '"
                     + params[idx2].getValue() + "' not a number");
               }
               curr.expires = new Date(System.currentTimeMillis() + age * 1000L);
            }
            else if (name.equals("port")) // Port
            {
               if (curr.port_set)
                  continue;

               if (params[idx2].getValue() == null)
               {
                  curr.port_list = new int[1];
                  curr.port_list[0] = req.getConnection().getPort();
                  curr.port_set = true;
                  continue;
               }

               curr.port_list_str = params[idx2].getValue();
               StringTokenizer tok = new StringTokenizer(params[idx2].getValue(), ",");
               curr.port_list = new int[tok.countTokens()];
               for (int idx3 = 0; idx3 < curr.port_list.length; idx3++)
               {
                  String port = tok.nextToken().trim();
                  try
                  {
                     curr.port_list[idx3] = Integer.parseInt(port);
                  }
                  catch (NumberFormatException nfe)
                  {
                     throw new ProtocolException("Bad Set-Cookie2 header: " + set_cookie + "\nPort '" + port
                        + "' not a number");
                  }
               }
               curr.port_set = true;
            }
            else if (name.equals("discard")) // Domain
            {
               if (discard_set)
                  continue;
               curr.discard = true;
               discard_set = true;
            }
            else if (name.equals("secure")) // Secure
            {
               if (secure_set)
                  continue;
               curr.secure = true;
               secure_set = true;
            }
            else if (name.equals("comment")) // Comment
            {
               if (curr.comment != null)
                  continue;
               try
               {
                  curr.comment = new String(params[idx2].getValue().getBytes("8859_1"), "UTF8");
               }
               catch (UnsupportedEncodingException usee)
               {
                  throw new Error(usee.toString()); /* shouldn't happen */
               }
            }
            else if (name.equals("commenturl")) // CommentURL
            {
               if (curr.comment_url != null)
                  continue;
               try
               {
                  curr.comment_url = new URI(params[idx2].getValue());
               }
               catch (ParseException pe)
               {
                  throw new ProtocolException("Bad Set-Cookie2 header: " + set_cookie + "\nCommentURL '"
                     + params[idx2].getValue() + "' not a valid URL");
               }
            }
            // ignore unknown element
         }

         // check version

         if (curr.version == -1)
            continue;

         // setup defaults

         if (curr.expires == null)
            curr.discard = true;

         // check validity

         // path attribute must be a prefix of the request-URI
         if (!Util.getPath(req.getRequestURI()).startsWith(curr.path))
         {
            log.warn("Bad Set-Cookie2 header: " + set_cookie + ", path '" + curr.path + "' is not a prefix of the "
               + "request uri '" + req.getRequestURI() + "'");
            continue;
         }

         // if host name is simple (i.e w/o a domain) then append .local
         String eff_host = req.getConnection().getHost();
         if (eff_host.indexOf('.') == -1)
            eff_host += ".local";

         // domain must be either .local or must contain at least two dots
         if (!curr.domain.equals(".local") && curr.domain.indexOf('.', 1) == -1)
         {
            log.warn("Bad Set-Cookie2 header: " + set_cookie + ", domain '" + curr.domain + "' is not '.local' and "
               + "doesn't contain two '.'s");
            continue;
         }

         // domain must domain match host
         if (!eff_host.endsWith(curr.domain))
         {
            log.warn("Bad Set-Cookie2 header: " + set_cookie + ", domain '" + curr.domain + "' does not match current"
               + "host '" + eff_host + "'");
            continue;
         }

         // host minus domain may not contain any dots
         if (eff_host.substring(0, eff_host.length() - curr.domain.length()).indexOf('.') != -1)
         {
            log.warn("Bad Set-Cookie2 header: " + set_cookie + ", domain '" + curr.domain + "' is more than one '.'"
               + "away from host '" + eff_host + "'");
            continue;
         }

         // if a port list is given it must include the current port
         if (curr.port_set)
         {
            int idx2 = 0;
            for (idx2 = 0; idx2 < curr.port_list.length; idx2++)
               if (curr.port_list[idx2] == req.getConnection().getPort())
                  break;
            if (idx2 == curr.port_list.length)
            {
               log.warn("Bad Set-Cookie2 header: " + set_cookie + ", port list " + "does include current port "
                  + req.getConnection().getPort());
               continue;
            }
         }

         // looks ok

         cookie_arr[cidx++] = curr;
      }

      if (cidx < cookie_arr.length)
         cookie_arr = Util.resizeArray(cookie_arr, cidx);

      return cookie_arr;
   }

   /**
    * @return the version as an int
    */
   public int getVersion()
   {
      return version;
   }

   /**
    * @return the comment string, or null if none was set
    */
   public String getComment()
   {
      return comment;
   }

   /**
    * @return the comment url
    */
   public URI getCommentURL()
   {
      return comment_url;
   }

   /**
    * @return the array of ports
    */
   public int[] getPorts()
   {
      return port_list;
   }

   /**
    * @return true if the cookie should be discarded at the end of the session;
    *         false otherwise
    */
   public boolean discard()
   {
      return discard;
   }

   /**
    * @param req the request to be sent
    * @return true if this cookie should be sent with the request
    */
   protected boolean sendWith(RoRequest req)
   {
      HTTPConnection con = req.getConnection();

      boolean port_match = !port_set;
      if (port_set)
         for (int idx = 0; idx < port_list.length; idx++)
            if (port_list[idx] == con.getPort())
            {
               port_match = true;
               break;
            }

      String eff_host = con.getHost();
      if (eff_host.indexOf('.') == -1)
         eff_host += ".local";

      return ((domain.charAt(0) == '.' && eff_host.endsWith(domain) || domain.charAt(0) != '.'
         && eff_host.equals(domain))
         && port_match && Util.getPath(req.getRequestURI()).startsWith(path) && (!secure
         || con.getProtocol().equals("https") || con.getProtocol().equals("shttp")));
   }

   protected String toExternalForm()
   {
      StringBuffer cookie = new StringBuffer();

      if (version == 1)
      {
         /*
          * cookie.append("$Version="); cookie.append(version); cookie.append(";
          * ");
          */

         cookie.append(name);
         cookie.append("=");
         cookie.append(value);

         if (path_set)
         {
            cookie.append("; ");
            cookie.append("$Path=");
            cookie.append(path);
         }

         if (domain_set)
         {
            cookie.append("; ");
            cookie.append("$Domain=");
            cookie.append(domain);
         }

         if (port_set)
         {
            cookie.append("; ");
            cookie.append("$Port");
            if (port_list_str != null)
            {
               cookie.append("=\"");
               cookie.append(port_list_str);
               cookie.append('\"');
            }
         }
      }
      else
         throw new Error("Internal Error: unknown version " + version);

      return cookie.toString();
   }

   /**
    * Create a string containing all the cookie fields. The format is that used
    * in the Set-Cookie header.
    */
   public String toString()
   {
      StringBuffer res = new StringBuffer(name.length() + value.length() + 50);
      res.append(name).append('=').append(value);

      if (version == 1)
      {
         res.append("; Version=").append(version);
         res.append("; Path=").append(path);
         res.append("; Domain=").append(domain);
         if (port_set)
         {
            res.append("; Port=\"").append(port_list[0]);
            for (int idx = 1; idx < port_list.length; idx++)
               res.append(',').append(port_list[idx]);
            res.append('\"');
         }
         if (expires != null)
            res.append("; Max-Age=").append(((expires.getTime() - System.currentTimeMillis()) / 1000L));
         if (discard)
            res.append("; Discard");
         if (secure)
            res.append("; Secure");
         if (comment != null)
            res.append("; Comment=\"").append(comment).append('\"');
         if (comment_url != null)
            res.append("; CommentURL=\"").append(comment_url).append('\"');
      }
      else
         throw new Error("Internal Error: unknown version " + version);

      return res.toString();
   }
}
